/****************************************************************************
 * Copyright (c) 2008 Mustafa K. Isik and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * Contributors:
 *    Mustafa K. Isik - initial API and implementation
 *
 * SPDX-License-Identifier: EPL-2.0
 *****************************************************************************/

package org.eclipse.ecf.internal.sync.doc.cola;

import java.util.*;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IAdapterManager;
import org.eclipse.ecf.core.identity.ID;
import org.eclipse.ecf.core.util.Trace;
import org.eclipse.ecf.internal.sync.Activator;
import org.eclipse.ecf.internal.sync.SyncDebugOptions;
import org.eclipse.ecf.sync.*;
import org.eclipse.ecf.sync.doc.DocumentChangeMessage;
import org.eclipse.ecf.sync.doc.IDocumentChange;
import org.eclipse.osgi.util.NLS;

public class ColaSynchronizationStrategy implements
		IModelSynchronizationStrategy {

	// <ColaDocumentChangeMessage>
	private final List unacknowledgedLocalOperations;
	private final boolean isInitiator;
	private long localOperationsCount;
	private long remoteOperationsCount;

	// <ID, ColaSynchronizationStrategy>
	private static Map sessionStrategies = new HashMap();

	private ColaSynchronizationStrategy(boolean isInitiator) {
		this.isInitiator = isInitiator;
		unacknowledgedLocalOperations = new LinkedList();
		localOperationsCount = 0;
		remoteOperationsCount = 0;
	}

	public static ColaSynchronizationStrategy getInstanceFor(ID client,
			boolean isInitiator) {
		ColaSynchronizationStrategy existingStrategy = (ColaSynchronizationStrategy) sessionStrategies
				.get(client);
		if (existingStrategy != null
				&& existingStrategy.isInitiator == isInitiator)
			return existingStrategy;
		existingStrategy = new ColaSynchronizationStrategy(isInitiator);
		sessionStrategies.put(client, existingStrategy);
		return existingStrategy;
	}

	public static void cleanUpFor(ID client) {
		sessionStrategies.remove(client);
	}

	public static void dispose() {
		sessionStrategies.clear();
	}

	/**
	 * Handles proper transformation of incoming
	 * <code>ColaDocumentChangeMessage</code>s. Returned
	 * <code>DocumentChangeMessage</code>s can be applied directly to the shared
	 * document. The method implements the concurrency algorithm described in
	 * <code>http://wiki.eclipse.org/RT_Shared_Editing</code>
	 * 
	 * @param remoteMsg
	 * @return List contains <code>DocumentChangeMessage</code>s ready for
	 *         sequential application to document
	 */
	public List transformIncomingMessage(final DocumentChangeMessage remoteMsg) {
		if (!(remoteMsg instanceof ColaDocumentChangeMessage)) {
			throw new IllegalArgumentException(
					"DocumentChangeMessage is incompatible with Cola SynchronizationStrategy"); //$NON-NLS-1$
		}
		Trace.entering(Activator.PLUGIN_ID, SyncDebugOptions.METHODS_ENTERING,
				this.getClass(), "transformIncomingMessage", remoteMsg); //$NON-NLS-1$
		ColaDocumentChangeMessage transformedRemote = (ColaDocumentChangeMessage) remoteMsg;

		final List transformedRemotes = new LinkedList();
		transformedRemotes.add(transformedRemote);

		remoteOperationsCount++;
		Trace.trace(Activator.PLUGIN_ID, "unacknowledgedLocalOperations="
				+ unacknowledgedLocalOperations);
		// this is where the concurrency algorithm is executed
		if (!unacknowledgedLocalOperations.isEmpty()) {// Do not remove this. It
														// is necessary. The
														// following iterator
														// does not suffice.
			// remove operations from queue that have been implicitly
			// acknowledged as received on the remote site by the reception of
			// this message
			for (final Iterator it = unacknowledgedLocalOperations.iterator(); it
					.hasNext();) {
				final ColaDocumentChangeMessage unackedLocalOp = (ColaDocumentChangeMessage) it
						.next();
				if (transformedRemote.getRemoteOperationsCount() > unackedLocalOp
						.getLocalOperationsCount()) {
					Trace.trace(
							Activator.PLUGIN_ID,
							NLS.bind(
									"transformIncomingMessage.removing {0}", unackedLocalOp)); //$NON-NLS-1$
					it.remove();
				} else {
					// the unackowledgedLocalOperations queue is ordered and
					// sorted
					// due to sequential insertion of local ops, thus once a
					// local op with a higher
					// or equal local op count (i.e. remote op count from the
					// remote operation's view)
					// is reached, we can abandon the check for the remaining
					// queue items
					Trace.trace(Activator.PLUGIN_ID,
							"breaking out of unackedLocalOperations loop"); //$NON-NLS-1$
					break;// exits for-loop
				}
			}

			// at this point the queue has been freed of operations that
			// don't require to be transformed against

			if (!unacknowledgedLocalOperations.isEmpty()) {
				ColaDocumentChangeMessage localOp = (ColaDocumentChangeMessage) unacknowledgedLocalOperations
						.get(0);
				Assert.isTrue(transformedRemote.getRemoteOperationsCount() == localOp
						.getLocalOperationsCount());

				for (final ListIterator unackOpsListIt = unacknowledgedLocalOperations
						.listIterator(); unackOpsListIt.hasNext();) {
					for (final ListIterator trafoRemotesIt = transformedRemotes
							.listIterator(); trafoRemotesIt.hasNext();) {
						// returns new instance
						// clarify operation preference, owner/docshare
						// initiator
						// consistently comes first
						localOp = (ColaDocumentChangeMessage) unackOpsListIt
								.next();
						transformedRemote = (ColaDocumentChangeMessage) trafoRemotesIt
								.next();
						transformedRemote = transformedRemote.transformAgainst(
								localOp, isInitiator);

						if (transformedRemote.isSplitUp()) {
							// currently this only happens for a remote deletion
							// that needs to be transformed against a locally
							// applied insertion
							// attention: before applying a list of deletions to
							// docshare, the indices need to be
							// updated/finalized one last time
							// since deletions can only be applied sequentially
							// and every deletion is going to change the
							// underlying document and the
							// respective indices!
							trafoRemotesIt.remove();
							for (final Iterator splitUpIterator = transformedRemote
									.getSplitUpRepresentation().iterator(); splitUpIterator
									.hasNext();) {
								trafoRemotesIt.add(splitUpIterator.next());
							}
							// according to the ListIterator documentation it
							// seems so as if the following line is unnecessary,
							// as a call to next() after the last removal and
							// additions will return what it would have returned
							// anyway
							// trafoRemotesIt.next();//TODO not sure about the
							// need for this - I want to jump over the two
							// inserted ops and reach the end of this iterator
						}

						// TODO check whether or not this collection shuffling
						// does what it is supposed to, i.e. remove current
						// localop in unack list and add split up representation
						// instead
						if (localOp.isSplitUp()) {
							// local operation has been split up during
							// operational transform --> remove current version
							// and add new versions plus jump over those
							unackOpsListIt.remove();
							for (final Iterator splitUpOpIterator = localOp
									.getSplitUpRepresentation().iterator(); splitUpOpIterator
									.hasNext();) {
								unackOpsListIt.add(splitUpOpIterator.next());
							}
							// according to the ListIterator documentation it
							// seems so as if the following line is unnecessary,
							// as a call to next() after the last removal and
							// additions will return what it would have returned
							// anyway
							// unackOpsListIt.next();//TODO check whether or not
							// this does jump over both inserted operations that
							// replaced the current unack op
						}// end split up localop handling
					}// transformedRemotes List iteration
				}
			}

		}
		Trace.exiting(Activator.PLUGIN_ID, SyncDebugOptions.METHODS_EXITING,
				this.getClass(), "transformIncomingMessage", transformedRemote); //$NON-NLS-1$

		// TODO find a cleaner and more OO way of cleaning up the list if it
		// contains multiple deletions:
		if (transformedRemotes.size() > 1) {
			final ColaDocumentChangeMessage firstOp = (ColaDocumentChangeMessage) transformedRemotes
					.get(0);
			if (firstOp.isDeletion()) {
				// this means all operations in the return list must also be
				// deletions, i.e. modify virtual/optimistic offset for
				// sequential application to document
				final ListIterator deletionFinalizerIt = transformedRemotes
						.listIterator();
				ColaDocumentChangeMessage previousDel = (ColaDocumentChangeMessage) deletionFinalizerIt
						.next();// jump over first del-op does not need
								// modification, we know this is OK because of
								// previous size check;
				ColaDocumentChangeMessage currentDel;

				for (; deletionFinalizerIt.hasNext();) {
					currentDel = (ColaDocumentChangeMessage) deletionFinalizerIt
							.next();
					currentDel.setOffset(currentDel.getOffset()
							- previousDel.getLengthOfReplacedText());
					previousDel = currentDel;
				}
			}
		}

		return transformedRemotes;
	}

	public String toString() {
		final StringBuffer buf = new StringBuffer("ColaSynchronizationStrategy"); //$NON-NLS-1$
		return buf.toString();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.eclipse.ecf.sync.doc.IDocumentSynchronizationStrategy#registerLocalChange
	 * (org.eclipse.ecf.sync.doc.IModelChange)
	 */
	public IModelChangeMessage[] registerLocalChange(IModelChange localChange) {
		List results = new ArrayList();
		Trace.entering(Activator.PLUGIN_ID, SyncDebugOptions.METHODS_ENTERING,
				this.getClass(), "registerLocalChange", localChange); //$NON-NLS-1$
		if (localChange instanceof IDocumentChange) {
			final IDocumentChange docChange = (IDocumentChange) localChange;
			final ColaDocumentChangeMessage colaMsg = new ColaDocumentChangeMessage(
					new DocumentChangeMessage(docChange.getOffset(),
							docChange.getLengthOfReplacedText(),
							docChange.getText()), localOperationsCount,
					remoteOperationsCount);
			// If not replacement, we simply add to
			// unacknowledgedLocalOperations and add message
			// to results
			if (!colaMsg.isReplacement()) {
				unacknowledgedLocalOperations.add(colaMsg);
				localOperationsCount++;
				results.add(colaMsg);
			} else {
				// It *is a replacement message, so we add both a delete and an
				// insert message
				// First create/add a delete message (text set to "")...
				ColaDocumentChangeMessage delMsg = new ColaDocumentChangeMessage(
						new DocumentChangeMessage(docChange.getOffset(),
								docChange.getLengthOfReplacedText(), ""),
						localOperationsCount, remoteOperationsCount);
				unacknowledgedLocalOperations.add(delMsg);
				localOperationsCount++;
				results.add(delMsg);
				// Then create/add the insert message (length set to 0)
				ColaDocumentChangeMessage insMsg = new ColaDocumentChangeMessage(
						new DocumentChangeMessage(docChange.getOffset(), 0,
								docChange.getText()), localOperationsCount,
						remoteOperationsCount);
				unacknowledgedLocalOperations.add(insMsg);
				localOperationsCount++;
				results.add(insMsg);
			}
			Trace.exiting(Activator.PLUGIN_ID,
					SyncDebugOptions.METHODS_EXITING, this.getClass(),
					"registerLocalChange", colaMsg); //$NON-NLS-1$
		}
		return (IModelChangeMessage[]) results
				.toArray(new IModelChangeMessage[] {});
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.ecf.sync.doc.IDocumentSynchronizationStrategy#
	 * toDocumentChangeMessage(byte[])
	 */
	public IModelChange deserializeRemoteChange(byte[] bytes)
			throws SerializationException {
		return DocumentChangeMessage.deserialize(bytes);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.ecf.sync.doc.IDocumentSynchronizationStrategy#
	 * transformRemoteChange(org.eclipse.ecf.sync.doc.IModelChangeMessage)
	 */
	public IModelChange[] transformRemoteChange(IModelChange remoteChange) {
		if (!(remoteChange instanceof DocumentChangeMessage))
			return new IDocumentChange[0];
		final DocumentChangeMessage m = (DocumentChangeMessage) remoteChange;
		final List l = this.transformIncomingMessage(m);
		return (IDocumentChange[]) l.toArray(new IDocumentChange[] {});
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class)
	 */
	public Object getAdapter(Class adapter) {
		if (adapter == null)
			return null;
		IAdapterManager manager = Activator.getDefault().getAdapterManager();
		if (manager == null)
			return null;
		return manager.loadAdapter(this, adapter.getName());
	}
}